﻿using GanFeng.Forest.WebApi.Configs;
using GanFeng.Forest.WebApi.Exceptions;
using GanFeng.Forest.WebApi.Objects;
using hyjx.comm;
using hyjx.comm.Exceptions;
using hyjx.comm.Interfaces;
using hyjx.comm.Types;
using Hyjx.GanFengLogger;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using S7.Net;
using System.Collections;
using System.Data;
using System.Diagnostics;
using System.Drawing;
using System.Net;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;


namespace GanFeng.Forest.WebApi.Controllers
{
    /// <summary>
    /// 搅拌机控制器
    /// </summary>
    [Route("api/[controller]")]
    [ApiController]
    public class MixerController : ApiControllerBase<MixerController>
    {
        IS7Protocol? _plc;
        readonly int _readGlueDb;
        readonly int _readGlueCommand;
        readonly int _readGlueCommand2;
        readonly int _readGlueRecipeNo;
        readonly int _writeRecipeDb;
        readonly int _readRecipeDb;
        readonly int _readBoolResult;
        readonly int _writeBoolResult;
        readonly short _dilutionStep1Material; // 稀释工步一物料
        readonly short _dilutionStep2Material; // 稀释工步二物料

        /// <summary>
        /// 创建一个新的搅拌机控制器
        /// </summary>
        /// <param name="logger">日志</param>
        /// <param name="config">配置</param>
        public MixerController(ILogger<MixerController> logger, IConfiguration config) : base(logger, config)
        {
            _readGlueDb = Config.GetValue<int>("RecipePoint:ReadRecipe:Glue:DataBlock");
            _readGlueCommand = Config.GetValue<int>("RecipePoint:ReadRecipe:Glue:Command");
            _readGlueCommand2 = Config.GetValue<int>("RecipePoint:ReadRecipe:Glue:Command2");
            _readGlueRecipeNo = Config.GetValue<int>("RecipePoint:ReadRecipe:Glue:RecipeNo");
            _writeRecipeDb = Config.GetValue<int>("RecipePoint:WriteRecipe:Mixer:DataBlock");
            _readRecipeDb = Config.GetValue<int>("RecipePoint:ReadRecipe:Mixer:DataBlock");
            _writeBoolResult = Config.GetValue<int>("RecipePoint:WriteRecipe:Mixer:BoolField");
            _readBoolResult = Config.GetValue<int>("RecipePoint:ReadRecipe:Mixer:BoolField");
            _dilutionStep1Material = Config.GetValue<short>("MaterialCode:Dilution:Step1Material");
            _dilutionStep2Material = Config.GetValue<short>("MaterialCode:Dilution:Step2Material");
        }

        /// <summary>
        /// 设置配方，接口 Id: 201
        /// </summary>
        /// <param name="args">参数</param>
        /// <response code="200">执行完毕</response>
        [HttpPost(nameof(SetRecipe))]
        [ProducesResponseType(StatusCodes.Status200OK)]
        public async Task<ActionResult> SetRecipe(MesRecipeArgs args)
        {
            LogObject log = InitializeLog(args.DeviceCode, 201, args);
            try
            {
                if (args.Details == null)
                    throw new EnsureArgsAndCovertMaterialCodeException(1);

                PlcConfig device = GetDevicePlc(args.DeviceCode); // 拿到设备所属 PLC
                if (device.FeatureType == PlcFeatureTypes.Mix || device.FeatureType == PlcFeatureTypes.Frl)
                {
                    EnsureArgsAndCovertMaterialCode(args, device.FeatureType); // 确认参数
                }

                int maxStep = MaxStep;
                int realTimeDbBlock = Config.GetValue<int>("RecipePoint:ReadRealTime:Mixer:DataBlock");

                _plc = new S7Protocol(device.CpuType, device.Ip, device.Rack, device.Slot);
                // 如果是搅拌机，如果没启用 MES 模式，报错
                if (device.FeatureType == PlcFeatureTypes.Mix || device.FeatureType == PlcFeatureTypes.Frl)
                {
                    string? address = Config["RecipePoint:ReadRealTime:Mixer:MesMode"];
                    if (string.IsNullOrEmpty(address))
                        return BadResult(3, log); // 服务配置错误

                    bool mesMode = _plc.ReadBool($"DB{realTimeDbBlock}.DBX{address}");
                    if (!mesMode)
                        return BadResult(21, log);
                }
                else
                    maxStep = 10; // 制胶和稀释最大工步是 10

                if (device.FeatureType == PlcFeatureTypes.Mix || device.FeatureType == PlcFeatureTypes.Frl) // 搅拌机或 FRL 搅拌机写入配方
                {
                    // 检查配方工步数，不足最大工步，补足空工步
                    int stepCount = args.Details.Count;
                    if (stepCount < maxStep)
                    {
                        int addStepCount = maxStep - stepCount;
                        for (int i = 0; i < addStepCount; i++)
                        {
                            args.Details.Add(new MesRecipeArgsDetail()
                            {
                                ElapsedTime = 0,
                                EndSymbol = false,
                                FunctionCode = 0,
                                MaterialCode = 0,
                                MaterialWeight = 0,
                                ScatteredRotateSpeed = 0,
                                StepSerialNumber = (byte)(stepCount + i + 1),
                                StirDirectionIsReverse = false,
                                StirRotateSpeed = 0,
                                Temperature = 0,
                                Vacuum = 0
                            });
                        }
                    }

                    // 对 PLC 写入数据
                    try
                    {
                        int materialNameByteStart = Config.GetValue<int>("RecipePoint:WriteRecipe:Mixer:StepStart"); // 原材料名称
                        //int materialWeightByteStart = MaxStep * 2; // 原材料设定重量
                        //int scatteredRotateSpeedStart = MaxStep * 6; // 分散转速设定
                        //int stirRotateSpeedStart = MaxStep * 8; // 搅拌转速设定
                        //int elapsedTimeStart = MaxStep * 10; // 运行时间设定
                        //int vacuumStart = MaxStep * 12; // 真空设定
                        //int temperatureStart = MaxStep * 16; // 温度设定
                        //float stirDirectionStart = MaxStep * 20; // 搅拌方向设定
                        //int functionCodeStart = (MaxStep * 20) + 4; // 功能码
                        //float endSymbolStart = (MaxStep * 22) + 4; // end 设定

                        //foreach (var item in args.Details)
                        //{
                        //    _plc.Write($"DB{_writeRecipeDb}.DBW{materialNameByteStart}", item.MaterialCode);
                        //    materialNameByteStart += 2;
                        //    _plc.Write($"DB{_writeRecipeDb}.DBD{materialWeightByteStart}", item.MaterialWeight);
                        //    materialWeightByteStart += 4;
                        //    _plc.Write($"DB{_writeRecipeDb}.DBW{scatteredRotateSpeedStart}", item.ScatteredRotateSpeed);
                        //    scatteredRotateSpeedStart += 2;
                        //    _plc.Write($"DB{_writeRecipeDb}.DBW{stirRotateSpeedStart}", item.StirRotateSpeed);
                        //    stirRotateSpeedStart += 2;
                        //    _plc.Write($"DB{_writeRecipeDb}.DBW{elapsedTimeStart}", item.ElapsedTime);
                        //    elapsedTimeStart += 2;
                        //    _plc.Write($"DB{_writeRecipeDb}.DBD{vacuumStart}", item.Vacuum);
                        //    vacuumStart += 4;
                        //    _plc.Write($"DB{_writeRecipeDb}.DBD{temperatureStart}", item.Temperature);
                        //    temperatureStart += 4;
                        //    _plc.Write($"DB{_writeRecipeDb}.DBX{stirDirectionStart:F1}", item.StirDirectionIsReverse);
                        //    _plc.Write($"DB{_writeRecipeDb}.DBW{functionCodeStart}", item.FunctionCode);
                        //    functionCodeStart += 2;
                        //    _plc.Write($"DB{_writeRecipeDb}.DBX{endSymbolStart:F1}", item.EndSymbol);

                        //    if (item.StepSerialNumber == 8 || item.StepSerialNumber == 16)
                        //    {
                        //        stirDirectionStart = (int)(stirDirectionStart + 1);
                        //        endSymbolStart = (int)(endSymbolStart + 1);
                        //    }
                        //    else
                        //    {
                        //        stirDirectionStart = (float)(stirDirectionStart + 0.1);
                        //        endSymbolStart = (float)(endSymbolStart + 0.1);
                        //    }
                        //}

                        // 写配方名称
                        //byte recipeNameLength = Config.GetValue<byte>("RecipePoint:WriteRecipe:Mixer:RecipeNameLength"); // 配方名长度
                        //var temp = Encoding.ASCII.GetBytes(args.Name);
                        //List<byte> bytes = new() { recipeNameLength, (byte)temp.Length };
                        //bytes.AddRange(temp);

                        //int recipeNameStart = Config.GetValue<int>("RecipePoint:WriteRecipe:Mixer:RecipeNameStart"); // 配方名起始位置
                        //_plc.Write(_writeRecipeDb, recipeNameStart, bytes.ToArray());
                        //_plc.Write($"DB{_writeRecipeDb}.DBX{_writeBoolResult}.0", true);// 下发配方同时要改写配方下发 = true

                        List<byte> bs = new();
                        foreach (var item in args.Details)
                        {
                            bs.AddRange(BitConverter.GetBytes(item.MaterialCode).Reverse()); // 原材料（物料）编码
                        }
                        foreach (var item in args.Details)
                        {
                            bs.AddRange(BitConverter.GetBytes(item.MaterialWeight).Reverse()); // 原材料设定重量
                        }
                        foreach (var item in args.Details)
                        {
                            bs.AddRange(BitConverter.GetBytes(item.ScatteredRotateSpeed).Reverse()); // 分散转速设定
                        }
                        foreach (var item in args.Details)
                        {
                            bs.AddRange(BitConverter.GetBytes(item.StirRotateSpeed).Reverse()); // 搅拌转速设定
                        }
                        foreach (var item in args.Details)
                        {
                            bs.AddRange(BitConverter.GetBytes(item.ElapsedTime).Reverse()); // 运行时间设定
                        }
                        foreach (var item in args.Details)
                        {
                            // 将MES的单位Mpa转为设备单位Kpa
                            bs.AddRange(BitConverter.GetBytes(item.Vacuum * 1000).Reverse());
                        }
                        foreach (var item in args.Details)
                        {
                            bs.AddRange(BitConverter.GetBytes(item.Temperature).Reverse()); // 温度设定
                        }
                        List<bool> bools = args.Details.Select(p => p.StirDirectionIsReverse).ToList();
                        byte[] boolBytes = ConvertBoolsToBytes(bools);
                        bs.AddRange(boolBytes);// 搅拌方向设定

                        byte[] emptyBytes = new byte[1];
                        bs.AddRange(emptyBytes); // PLC 空了一个字节，补上

                        foreach (var item in args.Details)
                        {
                            bs.AddRange(BitConverter.GetBytes(item.FunctionCode).Reverse()); // 功能码
                        }

                        bools = args.Details.Select(p => p.EndSymbol).ToList();
                        byte[] blBytes = ConvertBoolsToBytes(bools);
                        bs.AddRange(blBytes);// end 设定

                        emptyBytes = new byte[1];
                        bs.AddRange(emptyBytes); // PLC 空了一个字节，补上

                        bs.AddRange(GetStringBytes(args.Name, 24)); // 配方名称

                        // 配方下发，MES 在线写 true
                        bools = new List<bool>() { true, true };
                        byte[] bls = ConvertBoolsToBytes(bools);
                        bs.AddRange(bls);// 配方下发，MES 在线所在字节

                        _plc.Write(_writeRecipeDb, materialNameByteStart, bs.ToArray()); // 批量写入配方
                        //_plc.Write($"DB{_writeRecipeDb}.DBW{materialNameByteStart}", item.MaterialCode);

                        // 三秒内轮询是否下发配方成功
                        string searchAddress = $"DB{_readRecipeDb}.DBX{_readBoolResult}";
                        int timeOut = 1;
                        bool isWriteRecipeOk = _plc.ReadBool($"{searchAddress}.0");
                        while (timeOut <= 3)
                        {
                            if (isWriteRecipeOk)
                                break;

                            timeOut++;
                            await Task.Delay(TimeSpan.FromSeconds(1));
                            isWriteRecipeOk = _plc.ReadBool($"{searchAddress}.0");
                        }
                        MesVisitSucceed += $"配方是否下发成功点位:{searchAddress}.0,值:{isWriteRecipeOk}";
                        if (!isWriteRecipeOk)
                        {
                            byte[] readBytes = _plc.ReadBytes(_readRecipeDb, _readBoolResult, 1);
                            List<bool> readResultBools = readBytes.FromBytesToBools();
                            if (readResultBools[1] || readResultBools[5])
                                return BadResult(7, log);

                            if (readResultBools[2])
                                return BadResult(8, log);

                            if (readResultBools[3])
                                return BadResult(9, log);

                            if (readResultBools[4])
                                return BadResult(10, log);

                            return BadResult(12, log);
                        }

                        return OkResult(log);
                    }
                    catch (Exception ex)
                    {
                        int code = 6;
                        switch (ex.Message)
                        {
                            case nameof(S7ProtocolReadException):
                            case nameof(S7ProtocolWriteException):
                                code = 11;
                                break;
                            default:
                                break;
                        }
                        return ExceptionResult(code, ex, log);
                    }
                    finally
                    {
                        _plc.Dispose();
                    }
                }
                else // 制胶或稀释 MES 配方
                {
                    if (GlueMaterials == null || GlueMaterials.Length == 0)
                        return BadResult(48, log);

                    if (string.IsNullOrEmpty(device.MesMode))
                        return BadResult(3, log); // 服务配置错误

                    bool mesMode = _plc.ReadBool(device.MesMode);
                    if (!mesMode)
                        return BadResult(21, log);

                    if (device.FeatureType == PlcFeatureTypes.Glue && SystemSetting != null && SystemSetting.IsGlueEnabled) // 制胶配方下发
                    {
                        foreach (var item in args.Details)
                        {
                            if (item.FunctionCode == 2) // 制胶（稀释）工步，功能码不接受慢搅拌设定
                                return BadResult(26, log);

                            if (item.MaterialCode == 0) // 非加料工步：搅拌转速限定 2 - 40；分散转速限定：80 - 1450
                            {
                                if (item.StirRotateSpeed < 2 || item.StirRotateSpeed > 40)
                                    return BadResult(27, log);

                                //if (item.ScatteredRotateSpeed < 80 || item.ScatteredRotateSpeed > 1450)
                                //    return BadResult(28, log);

                                item.Vacuum *= 1000; // 上料系统的压力单位是 Kpa，接口给的压力单位是 Mpa，所以要转换
                            }
                            else // 加料工步
                            {
                                if (item.StirRotateSpeed < 2 || item.StirRotateSpeed > 16)
                                    return BadResult(29, log);
                                if (item.ScatteredRotateSpeed < 80 || item.ScatteredRotateSpeed > 500)
                                    return BadResult(30, log);
                                if (item.Vacuum != 0)
                                    return BadResult(31, log);
                                if (item.ElapsedTime != 0)
                                    return BadResult(32, log);

                                var mat = GlueMaterials.FirstOrDefault(p => p.HongYunValue == item.MaterialCode);
                                if (mat != null)
                                {
                                    item.MaterialCode = mat.PowderPlcValue;
                                }
                                else
                                {
                                    return BadResult(49, log);
                                }
                            }
                        }

                        // 以刘国强的制胶逻辑来编写下面的代码
                        string? glueVolumeAddress = Config["RecipePoint:ReadRealTime:Glue:VolumeAddress"];
                        if (string.IsNullOrEmpty(glueVolumeAddress))
                            return BadResult(3, log); // 配置错误

                        int glueVolumeThreshold = Config.GetValue<int>("RecipePoint:ReadRealTime:Glue:VolumeThreshold"); // 制胶液位阈值

                        try
                        {
                            float glueVolumne = _plc.ReadFloat(glueVolumeAddress);
                            if (glueVolumne > glueVolumeThreshold)
                                return BadResult(33, log); // 制胶机液位过高

                            int glueWriteDb = Config.GetValue<int>("RecipePoint:WriteRecipe:Glue:DataBlock");
                            int glueRecipeNameStart = Config.GetValue<int>("RecipePoint:WriteRecipe:Glue:RecipeNameStart");
                            byte glueRecipeNameLength = Config.GetValue<byte>("RecipePoint:WriteRecipe:Glue:RecipeNameLength");
                            List<byte> fontBytes = new();
                            fontBytes.AddRange(GetStringBytes(args.Name, glueRecipeNameLength)); // 配方名称
                            short stepCount = (short)args.Details.Count;
                            fontBytes.AddRange(BitConverter.GetBytes(stepCount).Reverse());// 最大工步号
                            _plc.Write(glueWriteDb, glueRecipeNameStart, fontBytes.ToArray());

                            // 检查配方工步数，不足 10 工步，补足空工步
                            if (stepCount < maxStep)
                            {
                                int addStepCount = maxStep - stepCount;
                                for (int i = 0; i < addStepCount; i++)
                                {
                                    args.Details.Add(new MesRecipeArgsDetail()
                                    {
                                        ElapsedTime = 0,
                                        EndSymbol = false,
                                        FunctionCode = 0,
                                        MaterialCode = 0,
                                        MaterialWeight = 0,
                                        ScatteredRotateSpeed = 0,
                                        StepSerialNumber = (byte)(stepCount + i + 1),
                                        StirDirectionIsReverse = false,
                                        StirRotateSpeed = 0,
                                        Temperature = 0,
                                        Vacuum = 0
                                    });
                                }
                            }

                            int materialCodeIndex = Config.GetValue<int>("RecipePoint:WriteRecipe:Glue:StepStart");

                            List<byte> afterBytes = new();
                            byte[] emptyBytes = new byte[4];
                            byte[] empty2Bytes = new byte[2];
                            foreach (var item in args.Details)
                            {
                                afterBytes.AddRange(BitConverter.GetBytes(item.MaterialCode).Reverse());// 物料名称
                                afterBytes.AddRange(BitConverter.GetBytes(item.MaterialWeight).Reverse());// 物料设定重量
                                afterBytes.AddRange(emptyBytes); // 空四个字节
                                afterBytes.AddRange(BitConverter.GetBytes(item.Temperature).Reverse()); // 设定温度
                                afterBytes.AddRange(emptyBytes); // 空四个字节
                                afterBytes.AddRange(BitConverter.GetBytes(item.Vacuum).Reverse()); // 设定真空
                                afterBytes.AddRange(emptyBytes); // 空四个字节
                                afterBytes.AddRange(BitConverter.GetBytes((float)item.StirRotateSpeed).Reverse()); // 设定搅拌转速
                                afterBytes.AddRange(BitConverter.GetBytes((float)item.ScatteredRotateSpeed).Reverse()); // 设定分散转速
                                afterBytes.AddRange(BitConverter.GetBytes(item.ElapsedTime).Reverse()); // 设定时间（分钟）
                                afterBytes.AddRange(empty2Bytes); // 空两个字节
                                var boolBytes = ConvertBoolsToBytes(new List<bool>() { item.FunctionCode == 1, item.EndSymbol }); // 功能码 和 配方结束标记
                                afterBytes.AddRange(boolBytes);
                                afterBytes.AddRange(new byte[1]); // 空一个字节
                            }
                            _plc.Write(glueWriteDb, materialCodeIndex, afterBytes.ToArray());

                            Thread thread = new(() =>
                            {
                                Thread.Sleep(5000);// 5 秒后设置装载配方，此为 PLC 逻辑要求
                                string commandAddress = $"DB{_readGlueDb}.DBW{_readGlueCommand}";
                                string recipeNoAddress = $"DB{_readGlueDb}.DBW{_readGlueRecipeNo}";
                                // 参数说明：
                                // 参数 1 表示要设置装载配方
                                // 参数 2 表示 CPU 类型
                                // 参数 3 表示 CPU IP
                                // 参数 4 表示 CPU 机架
                                // 参数 5 表示 CPU 插槽
                                // 参数 6 表示 制胶装载配方地址
                                // 参数 7 表示制胶配方号（索引）
                                ExecuteFeedingSystemTimedTask($"1,{(int)device.CpuType},{device.Ip},{device.Rack},{device.Slot},{commandAddress},{recipeNoAddress}");

                            });
                            thread.Start();

                            return OkResult(log);
                        }
                        catch (Exception ex)
                        {
                            int code = 6;
                            switch (ex.Message)
                            {
                                case nameof(S7ProtocolWriteException):
                                    code = 11;
                                    break;
                                default:
                                    break;
                            }
                            return ExceptionResult(code, ex, log);
                        }
                        finally
                        {
                            _plc.Dispose();
                        }

                    }
                    else if (device.FeatureType == PlcFeatureTypes.Dilution && SystemSetting != null && SystemSetting.IsDilutionEnabled) // 稀释配方下发
                    {
                        string folder = $"{Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData)}\\hyjx";
                        string pathStart1 = $"{folder}\\{args.DeviceCode}_recipe_step1_start";
                        if (System.IO.File.Exists(pathStart1))
                        {
                            System.IO.File.Delete(pathStart1); // 删除文件
                        }
                        string pathStart2 = $"{folder}\\{args.DeviceCode}_recipe_step2_start";
                        if (System.IO.File.Exists(pathStart2))
                        {
                            System.IO.File.Delete(pathStart2); // 删除文件
                        }

                        var dilutionWriteRecipes = Config.GetSection("RecipePoint:WriteRecipe:Dilution").Get<DilutionWriteRecipe[]>();
                        var writeRecipe = dilutionWriteRecipes?.FirstOrDefault(r => r.DeviceCode == args.DeviceCode);
                        int writeDataBlock = writeRecipe?.DataBlock ?? throw new ArgumentException("RecipePoint:WriteRecipe:Dilution:DataBlock");
                        //int materialRecipeRatio = Config.GetValue<int>("RecipePoint:WriteRecipe:Dilution:MaterialRecipeRatio");
                        //int waterRecipeRatio = Config.GetValue<int>("RecipePoint:WriteRecipe:Dilution:WaterRecipeRatio");
                        //int writeRecipeName = Config.GetValue<int>("RecipePoint:WriteRecipe:Dilution:RecipeNameStart");
                        byte writeRecipeNameLength = (byte)writeRecipe!.RecipeNameLength;
                        //int writeStirRotateSpeed = Config.GetValue<int>("RecipePoint:WriteRecipe:Dilution:StirRotateSpeed");
                        //int writeStepElapsedTime = Config.GetValue<int>("RecipePoint:WriteRecipe:Dilution:StepElapsedTime");
                        //int writeVacuum = Config.GetValue<int>("RecipePoint:WriteRecipe:Dilution:Vacuum");
                        //int writeMaterialWeight = Config.GetValue<int>("RecipePoint:WriteRecipe:Dilution:MaterialWeight");
                        //int writeWaterWeight = Config.GetValue<int>("RecipePoint:WriteRecipe:Dilution:WaterWeight");
                        //int writeRecipeFlag = Config.GetValue<int>("RecipePoint:WriteRecipe:Dilution:WriteRecipeFlag");
                        var recipeRealTimes = Config.GetSection("RecipePoint:ReadRealTime:Dilution").Get<DilutionReadRealTime[]>();
                        var recipeRealTime = recipeRealTimes?.FirstOrDefault(r => r.DeviceCode == args.DeviceCode);

                        int realTimeDataBlock = recipeRealTime?.DataBlock ?? throw new ArgumentNullException("RecipePoint:ReadRealTime:Dilution:DataBlock");
                        int realTimeStep = recipeRealTime?.Step ?? throw new ArgumentNullException("RecipePoint:ReadRealTime:Dilution:DataBlock");

                        string stepAddress = $"DB{realTimeDataBlock}.DBW{realTimeStep}";

                        int step = _plc.ReadInt(stepAddress);
                        if (step != 0)
                            return BadResult(35, log); // 系统繁忙，不接收配方

                        List<byte> bytes = new();
                        bytes.AddRange(BitConverter.GetBytes((short)1).Reverse()); // 下发配方标志位置 1
                        bytes.AddRange(new byte[66]); // 空 66 字节
                        bytes.AddRange(GetStringBytes(args.Name, writeRecipeNameLength)); // 配方名，写完后字节数到 89
                        bytes.AddRange(new byte[76]); // 空 76 字节
                        bytes.AddRange(BitConverter.GetBytes((float)1).Reverse()); // 写物料配比
                        bytes.AddRange(BitConverter.GetBytes((float)args.Rate).Reverse()); // 写水配比
                        bytes.AddRange(new byte[28]); // 空 28 字节

                        // 记录配方 NMP(DIW) 重量，供读配方时用
                        MesRecipeArgsDetail? weight = args.Details.FirstOrDefault(p => p.MaterialCode == _dilutionStep2Material);
                        if (weight != null)
                            bytes.AddRange(BitConverter.GetBytes(weight.MaterialWeight).Reverse()); // 写水重量
                        else
                            bytes.AddRange(new byte[4]);

                        bytes.AddRange(new byte[8]); // 空 8 字节

                        // 记录配方 CNT(LA133) 重量，供读配方时用
                        MesRecipeArgsDetail? detail = args.Details.FirstOrDefault(p => p.MaterialCode == _dilutionStep1Material);
                        if (detail != null)
                            bytes.AddRange(BitConverter.GetBytes(detail.MaterialWeight).Reverse()); // 写物料重量
                        else
                            bytes.AddRange(new byte[4]);

                        bytes.AddRange(new byte[10]); // 空 10 字节

                        short stirRotateSpeed = args.Details.Max(p => p.StirRotateSpeed); // 搅拌转速
                        bytes.AddRange(BitConverter.GetBytes(stirRotateSpeed).Reverse());

                        bytes.AddRange(new byte[4]); // 空 4 字节
                        short stepTime = args.Details.Max(p => p.ElapsedTime); // 工步运行时间
                        bytes.AddRange(BitConverter.GetBytes(stepTime).Reverse());

                        bytes.AddRange(new byte[4]); // 空 4 字节
                        float vacuum = args.Details.Max(p => p.Vacuum); // 真空（压力）
                        bytes.AddRange(BitConverter.GetBytes(vacuum).Reverse());
                        _plc.Write(writeDataBlock, 0, bytes.ToArray());

                        Thread.Sleep(TimeSpan.FromSeconds(3)); // 等待三秒再检测

                        step = _plc.ReadInt(stepAddress);
                        if (step == 1) // 下发配方成功
                        {
                            using StreamWriter writer = new($"{AppDataFolder}\\{args.DeviceCode}_recipe", false); // 保存配方
                            writer.Write(log.RequestContent);
                            writer.Close();

                            return OkResult(log);
                        }
                        else
                            return BadResult(36, log); // 如果三秒后 Step 还没变成 1，肯定出问题了，返回错误，配方下发失败
                    }
                    else
                    {
                        return BadResult(3, log);
                    }
                }
            }
            catch (EnsureArgsAndCovertMaterialCodeException ex)
            {
                return BadResult(ex.Code, log);
            }
            catch (GetDevicePlcException ex)
            {
                return BadResult(ex.Code, log);
            }
            catch (Exception ex)
            {
                int code = 6;
                switch (ex.Message)
                {
                    case nameof(S7ProtocolReadException):
                    case nameof(S7ProtocolWriteException):
                        code = 11;
                        break;
                    default:
                        break;
                }
                return ExceptionResult(code, ex, log);
            }
            finally
            {
                _plc?.Dispose();
            }
        }

        /// <summary>
        /// 设置工步运行，接口 Id:202
        /// </summary>
        /// <param name="args"></param>
        /// <response code="200">执行完毕</response>
        /// <returns></returns>
        [HttpPost(nameof(SetStepRun))]
        [ProducesResponseType(StatusCodes.Status200OK)]
        public async Task<ActionResult> SetStepRun(SetStepRunArgs args)
        {
            LogObject log = InitializeLog(args.DeviceCode, 202, args);
            try
            {
                PlcConfig device = GetDevicePlc(args.DeviceCode); // 拿到 PLC
                EnsureArgsAndCovertMaterialCode(args, device.FeatureType); // 确认参数
                _plc = new S7Protocol(device.CpuType, device.Ip, device.Rack, device.Slot);
                if (device.FeatureType == PlcFeatureTypes.Mix || device.FeatureType == PlcFeatureTypes.Frl) // 发往搅拌机或 FRL
                {
                    string run = $"DB{_writeRecipeDb}.DBX{_writeBoolResult}.3";
                    var runValue = false;
                    for (int i = 0; i < 5; i++)
                    {
                        _plc.Write(run, args.Run); // 工步运行字段;
                        await Task.Delay(TimeSpan.FromMilliseconds(180));
                        runValue = _plc.ReadBool(run);
                        if (runValue) break;
                    }

                    MesVisitSucceed += $"Mes Value:{args.Run},Plc Address:{run},Value:{runValue}";
                    return OkResult(log);
                }
                else if (device.FeatureType == PlcFeatureTypes.Glue) // 如果是发往储罐 1500 CPU 的制胶的工步运行指令
                {
                    bool mesMode = _plc.ReadBool(device.MesMode);
                    if (!mesMode)
                        return BadResult(21, log);

                    // 查询新工步的物料，如果是 0 ，则表示非加料
                    int firstStepAddress = Config.GetValue<int>("RecipePoint:ReadRecipe:Glue:StepStart");
                    int oneStepLength = Config.GetValue<int>("RecipePoint:ReadRecipe:Glue:OneStepLength");
                    int materialAddress = (args.Step - 1) * oneStepLength + firstStepAddress;
                    int materialCode = _plc.ReadInt($"DB{_readGlueDb}.DBW{materialAddress}");
                    if (materialCode != 0) // 此工步是投料步骤
                    {
                        // 如果没有通过物料 MES 验证，则不允许执行工步运行
                        using StreamReader sr = new($"{AppDataFolder}\\{args.DeviceCode}");
                        string? line = sr.ReadLine();
                        sr.Close();
                        if (line == null)
                            return ExceptionResult(23, null, log);

                        MaterialMatchingObject? record = JsonSerializer.Deserialize<MaterialMatchingObject>(line);
                        if (record == null)
                            return ExceptionResult(22, null, log);

                        //if (record.StepNumber != args.Step) // 如果物料匹配的工步不是当前要运行的工步，不允许执行工步运行
                        //    return ExceptionResult(23, null, log);
                        //if (DateTime.Now - record.MatchingTime > TimeSpan.FromMinutes(60)) // 如果物料匹配下发时间超过 60 分钟，需重新设置物料匹配
                        //    return ExceptionResult(24, null, log);
                    }

                    float running = Config.GetValue<float>("RecipePoint:ReadRecipe:Glue:RecipeRunning");
                    bool recipeRunning = _plc.ReadBool($"DB{_readGlueDb}.DBX{running}.0");
                    if (!recipeRunning)
                        return BadResult(34, log);

                    var cmdAddr1 = $"DB{_readGlueDb}.DBW{_readGlueCommand}";
                    var cmdAddr2 = $"DB{_readGlueDb}.DBW{_readGlueCommand2}";

                    _plc.Write(cmdAddr1, 3);// 工步运行
                    _plc.Write(cmdAddr2, 3);

                    await Task.Delay(TimeSpan.FromMilliseconds(500));
                    var cmd1 = _plc.ReadInt(cmdAddr1);
                    var cmd2 = _plc.ReadInt(cmdAddr2);

                    MesVisitSucceed += $"CMD Address1:{cmdAddr1},Value1:{cmd1},Address2:{cmdAddr2}Value2:{cmd2}";

                    //_plc.Write($"DB{_readGlueDb}.DBW{_readGlueCommand}", 3);// 工步运行
                    //Thread.Sleep(TimeSpan.FromSeconds(3));
                    //_plc.Write($"DB{_readGlueDb}.DBW{_readGlueCommand}", 0);// 工步运行

                    return OkResult(log);
                }
                else
                {
                    return BadResult(3, log);
                }
            }
            catch (EnsureArgsAndCovertMaterialCodeException ex)
            {
                return BadResult(ex.Code, log);
            }
            catch (GetDevicePlcException ex)
            {
                return BadResult(ex.Code, log);
            }
            catch (Exception ex)
            {
                int code = 6;
                switch (ex.Message)
                {
                    case nameof(S7ProtocolReadException):
                    case nameof(S7ProtocolWriteException):
                        code = 13;
                        break;
                    default:
                        break;
                }
                return ExceptionResult(code, ex, log);
            }
            finally
            {
                _plc?.Dispose();
            }
        }

        /// <summary>
        /// 设置物料匹配，接口 Id:203
        /// </summary>
        /// <param name="args"></param>
        /// <response code="200">执行完毕</response>
        /// <returns></returns>
        [HttpPost(nameof(SetMaterialMatching))]
        [ProducesResponseType(StatusCodes.Status200OK)]
        public async Task<ActionResult> SetMaterialMatching(SetMaterialMatchingArgs args)
        {
            LogObject log = InitializeLog(args.DeviceCode, 203, args);
            try
            {
                PlcConfig device = GetDevicePlc(args.DeviceCode); // 拿到 PLC
                if (device.FeatureType == PlcFeatureTypes.Mix || device.FeatureType == PlcFeatureTypes.Frl)
                {
                    EnsureArgsAndCovertMaterialCode(args, device.FeatureType); // 确认参数
                }
                if (SystemSetting == null)
                    throw new Exception("SettingError");

                _plc = new S7Protocol(device.CpuType, device.Ip, device.Rack, device.Slot);

                if (device.FeatureType == PlcFeatureTypes.Mix || device.FeatureType == PlcFeatureTypes.Frl) // 发往搅拌机或 FRL
                {
                    // 检查配方里工步的物料和要下的物料是否匹配
                    int dbAddress = (args.StepNumber - 1) * 2;
                    int materialCode = _plc.ReadInt($"DB{_readRecipeDb}.DBW{dbAddress}");
                    MesVisitSucceed += $"PLC MaterialCode:{materialCode},MES MaterialCode:{args.MaterialCode}";

                    if (materialCode != args.MaterialCode)
                    {
                        _plc.Dispose();
                        return BadResult(19, log);
                    }

                    // M3 和 M5 往上料系统写入 主粉/EC Mes 验证结果
                    if ((Factory == 3 || Factory == 5) && ((args.MaterialCode == 1 && device.FeatureType == PlcFeatureTypes.Mix) || (!SystemSetting.IsPositive && args.MaterialCode == 10)))
                    {
                        string? mesAddress = null;
                        string? ecMesValidAddress = null;
                        FeedingConfig? feedingConfig = null;

                        if (!SystemSetting.IsFeedingEnabled || SystemSetting.Feedings == null || !SystemSetting.Feedings.Any())
                            throw new Exception("FeedingSettingEnabled");

                        foreach (var feeding in SystemSetting.Feedings.Where(p => p.IsEnabled))
                        {
                            if (feeding.RelatedMixers == null || !feeding.RelatedMixers.Any())
                                continue;

                            FeedingMixerConfig? mixer = feeding.RelatedMixers.FirstOrDefault(p => p.MixerCode == args.DeviceCode);
                            if (mixer == null)
                                continue;

                            mesAddress = mixer.MainPowderMesAddress;
                            ecMesValidAddress = mixer.EcMesValidAddress;
                            feedingConfig = feeding;
                            break;
                        }

                        if (feedingConfig == null)
                            return BadResult(3, log);

                        IS7Protocol feedingPlc = new S7Protocol(feedingConfig.CpuType, feedingConfig.Ip, feedingConfig.Rack, feedingConfig.Slot);

                        var mesValidAddr = string.Empty;
                        if (!SystemSetting.IsPositive && args.MaterialCode == 10 && !string.IsNullOrEmpty(ecMesValidAddress))
                        {
                            feedingPlc.Write(ecMesValidAddress, args.Result);
                            await Task.Delay(TimeSpan.FromMilliseconds(200));
                            var valid = feedingPlc.ReadBool(ecMesValidAddress);
                            MesVisitSucceed += $"MES valid address:{ecMesValidAddress},Value:{valid}";
                            mesValidAddr = ecMesValidAddress;
                        }
                        else if (!string.IsNullOrEmpty(mesAddress))
                        {
                            feedingPlc.Write(mesAddress, args.Result);
                            await Task.Delay(TimeSpan.FromMilliseconds(200));
                            var valid = feedingPlc.ReadBool(mesAddress);
                            MesVisitSucceed += $"MES valid address:{mesAddress},Value:{valid}";
                            mesValidAddr = mesAddress;
                        }

                        feedingPlc.Dispose();

                        if (!string.IsNullOrEmpty(mesValidAddr))
                        {
                            await Task.Run(() =>
                            {
                                // 参数说明：
                                // 参数 1 表示要监控 Mes 校验设置功能
                                // 参数 2 表示 CPU 类型
                                // 参数 3 表示 CPU IP
                                // 参数 4 表示 CPU 机架
                                // 参数 5 表示 CPU 插槽
                                // 参数 6 表示物料 Mes 验证结果地址
                                // 参数 7 表示 MES 验证通过持续分钟数
                                ExecuteFeedingSystemTimedTask($"4,{(int)feedingConfig.CpuType},{feedingConfig.Ip},{feedingConfig.Rack},{feedingConfig.Slot},{mesValidAddr},{SystemSetting?.MesValidKeepMinutes}");
                            });
                        }
                    }
                    return OkResult(log);
                }
                else // 制胶（稀释不调此接口）
                {
                    try
                    {
                        if (!args.Result) // 物料不匹配，不写物料匹配信息
                            return OkResult(log);
                        bool mesMode = _plc.ReadBool(device.MesMode);
                        if (!mesMode)
                            return BadResult(21, log);

                        using StreamWriter writer = new($"{AppDataFolder}\\{args.DeviceCode}"); // 以设备编码作为文件名，写入一个制胶物料匹配记录文件
                        MaterialMatchingObject obj = new() { StepNumber = args.StepNumber, Code = args.MaterialCode, MatchingTime = DateTime.Now };
                        writer.Write(JsonSerializer.Serialize(obj));
                        writer.Close();
                        return OkResult(log);
                    }
                    catch (Exception ex)
                    {
                        return ExceptionResult(6, ex, log);
                    }
                }
            }
            catch (EnsureArgsAndCovertMaterialCodeException ex)
            {
                return BadResult(ex.Code, log);
            }
            catch (GetDevicePlcException ex)
            {
                return BadResult(ex.Code, log);
            }
            catch (Exception ex)
            {
                int code = 3;
                switch (ex.Message)
                {
                    case nameof(S7ProtocolReadException):
                    case nameof(S7ProtocolWriteException):
                        code = 16;
                        break;
                    default:
                        break;
                }
                return ExceptionResult(code, ex, log);
            }
            finally
            {
                _plc?.Dispose();
            }
        }

        /// <summary>
        /// 读取当前配方，接口 Id:204
        /// </summary>
        /// <param name="device_code">设备编码</param>
        /// <returns></returns>
        [HttpGet(nameof(GetRecipe))]
        public ActionResult GetRecipe(string device_code)
        {
            LogObject log = InitializeLog(device_code, 204, device_code);
            try
            {
                PlcConfig device = GetDevicePlc(device_code); // 拿到 PLC
                _plc = new S7Protocol(device.CpuType, device.Ip, device.Rack, device.Slot);

                if (device.FeatureType == PlcFeatureTypes.Mix || device.FeatureType == PlcFeatureTypes.Frl) // 发往搅拌机或 FRL
                {
                    if (MaterialCodes == null)
                        return BadResult(48, log);

                    byte[] bytes;
                    int stepStart = Config.GetValue<int>("RecipePoint:ReadRecipe:Mixer:StepStart");
                    int stepLength = Config.GetValue<int>("RecipePoint:ReadRecipe:Mixer:StepLength");
                    bytes = _plc.ReadBytes(_readRecipeDb, stepStart, stepLength);

                    int stirDerect = Config.GetValue<int>("RecipePoint:ReadRecipe:Mixer:StepStirDirection"); // 工步搅拌方向字节位，共占 3 字节
                    byte[] stirDerectBytes = new byte[3];
                    Array.Copy(bytes, stirDerect, stirDerectBytes, 0, 3);
                    List<bool> stirDerectResults = stirDerectBytes.FromBytesToBools(); // 工步搅拌方向，24 个工步值

                    int endSymbol = Config.GetValue<int>("RecipePoint:ReadRecipe:Mixer:StepEndSymbol"); // 工步结束标识字节位，共占 3 字节
                    byte[] endSymbolBytes = new byte[3];
                    Array.Copy(bytes, endSymbol, endSymbolBytes, 0, 3);
                    List<bool> endSymbolResults = endSymbolBytes.FromBytesToBools(); // 工步结束标识，24 个工步值

                    List<bool> resultBits = bytes[_readBoolResult - 1].FromByteTo8Bool(); // BoolField 上的几个配方结果布尔值

                    int startNameIndex = Config.GetValue<int>("RecipePoint:ReadRecipe:Mixer:RecipeNameStart"); // 配方名起始位置
                    int nameLength = Config.GetValue<int>("RecipePoint:ReadRecipe:Mixer:RecipeNameLength"); // 配方名字节数

                    // 生成配方名字节流
                    var recipeName = string.Empty;
                    byte[] recipeNameBytes = _plc.ReadBytes(_readRecipeDb, startNameIndex, nameLength + 2);
                    if (recipeNameBytes[1] > 0)
                    {
                        byte[] nameBytes = new byte[recipeNameBytes[1]];
                        Array.Copy(recipeNameBytes, 2, nameBytes, 0, recipeNameBytes[1]);
                        recipeName = nameBytes.FromBytesToString(Encoding.UTF8);
                    }

                    MesRecipeResult result = new()
                    {
                        DeviceCode = device_code,
                        MaterialSettingWrong = resultBits[1],
                        ElapsedTimeSettingWrong = resultBits[2],
                        RotateSpeedSettingWrong = resultBits[3],
                        FunctionCodeSettingWrong = resultBits[4],
                        Name = recipeName,
                    };
                    List<MesRecipeResultDetail> details = new();

                    int materialNameByteStart = 0; // 原材料名称
                    int materialWeightByteStart = MaxStep * 2; // 原材料设定重量
                    int scatteredRotateSpeedStart = MaxStep * 6; // 分散转速设定
                    int stirRotateSpeedStart = MaxStep * 8; // 搅拌转速设定
                    int elapsedTimeStart = MaxStep * 10; // 运行时间设定
                    int vacuumStart = MaxStep * 12; // 真空设定
                    int temperatureStart = MaxStep * 16; // 温度设定
                    float stirDirectionStart = MaxStep * 20; // 搅拌方向设定
                    int functionCodeStart = (MaxStep * 20) + 4;// 功能码
                    float endSymbolStart = (MaxStep * 22) + 4;// End 设定

                    for (int i = 0; i < MaxStep; i++)
                    {
                        MesRecipeResultDetail detail = new()
                        {
                            StepSerialNumber = i + 1,
                            MaterialCode = new byte[] { bytes[materialNameByteStart], bytes[materialNameByteStart + 1] }.FromBytesToShort(),
                            MaterialWeight = new byte[] { bytes[materialWeightByteStart], bytes[materialWeightByteStart + 1], bytes[materialWeightByteStart + 2], bytes[materialWeightByteStart + 3] }.FromBytesToFloat(),
                            ScatteredRotateSpeed = new byte[] { bytes[scatteredRotateSpeedStart], bytes[scatteredRotateSpeedStart + 1] }.FromBytesToShort(),
                            StirRotateSpeed = new byte[] { bytes[stirRotateSpeedStart], bytes[stirRotateSpeedStart + 1] }.FromBytesToShort(),
                            ElapsedTime = new byte[] { bytes[elapsedTimeStart], bytes[elapsedTimeStart + 1] }.FromBytesToShort(),
                            Vacuum = new byte[] { bytes[vacuumStart], bytes[vacuumStart + 1], bytes[vacuumStart + 2], bytes[vacuumStart + 3] }.FromBytesToFloat() / 1000,
                            Temperature = new byte[] { bytes[temperatureStart], bytes[temperatureStart + 1], bytes[temperatureStart + 2], bytes[temperatureStart + 3] }.FromBytesToFloat(),
                            FunctionCode = new byte[] { bytes[functionCodeStart], bytes[functionCodeStart + 1] }.FromBytesToShort()
                        };

                        detail.MaterialCode = ConvertMaterialCode(detail.MaterialCode, device.FeatureType, false); // 将红运物料编码转换成赣锋物料编码
                        var material = MaterialCodes.FirstOrDefault(p => p.GanFengValue == detail.MaterialCode && p.FeatureType == device.FeatureType);
                        if (material == null)
                            return BadResult(49, log);

                        if (!string.IsNullOrEmpty(material.ToleranceUpperLimitAddress))
                            detail.MaterialWeightToleranceUpperlimit = _plc.ReadFloat(material.ToleranceUpperLimitAddress);

                        if (!string.IsNullOrEmpty(material.ToleranceLowerLimitAddress))
                            detail.MaterialWeightToleranceLowlimit = _plc.ReadFloat(material.ToleranceLowerLimitAddress);

                        detail.StirDirectionIsReverse = stirDerectResults[i];
                        detail.EndSymbol = endSymbolResults[i];

                        materialNameByteStart += 2;
                        materialWeightByteStart += 4;
                        scatteredRotateSpeedStart += 2;
                        stirRotateSpeedStart += 2;
                        elapsedTimeStart += 2;
                        vacuumStart += 4;
                        temperatureStart += 4;
                        functionCodeStart += 2;

                        details.Add(detail);
                    }

                    result.Details = details.ToArray();

                    log.ResponseContent = JsonSerializer.Serialize(result);
                    LogMes(MesVisitSucceed, log);
                    return Ok(result);
                }
                else if (device.FeatureType == PlcFeatureTypes.Glue) // 读上料系统 1500 CPU 的制胶配方
                {
                    if (GlueMaterials == null || GlueMaterials.Length == 0)
                        return BadResult(48, log);

                    int recipeNameIndex = Config.GetValue<int>("RecipePoint:ReadRecipe:Glue:RecipeNameStart"); // 配方名起始位置
                    int recipeNameLength = Config.GetValue<int>("RecipePoint:ReadRecipe:Glue:RecipeNameLength"); // 配方名字节数
                    var recipeName = string.Empty;
                    byte[] recipeNameBytes = _plc.ReadBytes(_readGlueDb, recipeNameIndex, recipeNameLength + 2);
                    if (recipeNameBytes[1] > 0)
                    {
                        byte[] nameBytes = new byte[recipeNameBytes[1]];
                        Array.Copy(recipeNameBytes, 2, nameBytes, 0, recipeNameBytes[1]);
                        recipeName = nameBytes.FromBytesToString(Encoding.UTF8);
                    }

                    MesRecipeResult recipe = new() { DeviceCode = device_code, Name = recipeName }; // 声明返回配方结果

                    int stepStart = Config.GetValue<int>("RecipePoint:ReadRecipe:Glue:StepStart");
                    int oneStepLength = Config.GetValue<int>("RecipePoint:ReadRecipe:Glue:OneStepLength");
                    int totalStepLength = oneStepLength * 10;
                    byte[] bytes = _plc.ReadBytes(_readGlueDb, stepStart, totalStepLength); // 取 10 个工步的字节。从 Recipe[1] 开始取第一工步
                    List<MesRecipeResultDetail> details = new();

                    int stepNumber = 1;
                    for (int i = 0; i < totalStepLength; i += oneStepLength)
                    {
                        List<bool> bools = bytes[i + 38].FromByteTo8Bool();

                        // 粉罐 PLC 没有记录公差值，无法读取寄存器拿到
                        MesRecipeResultDetail detail = new()
                        {
                            MaterialCode = new byte[] { bytes[i], bytes[i + 1] }.FromBytesToShort(),
                            MaterialWeight = new byte[] { bytes[i + 2], bytes[i + 3], bytes[i + 4], bytes[i + 5] }.FromBytesToFloat(),
                            Temperature = new byte[] { bytes[i + 10], bytes[i + 11], bytes[i + 12], bytes[i + 13] }.FromBytesToFloat(),
                            Vacuum = new byte[] { bytes[i + 18], bytes[i + 19], bytes[i + 20], bytes[i + 21] }.FromBytesToFloat(),
                            StirRotateSpeed = (short)new byte[] { bytes[i + 26], bytes[i + 27], bytes[i + 28], bytes[i + 29] }.FromBytesToFloat(),
                            ScatteredRotateSpeed = (short)new byte[] { bytes[i + 30], bytes[i + 31], bytes[i + 32], bytes[i + 33] }.FromBytesToFloat(),
                            ElapsedTime = new byte[] { bytes[i + 34], bytes[i + 35] }.FromBytesToShort(),
                            FunctionCode = bools[0] == true ? (short)1 : (short)0,
                            EndSymbol = bools[1]
                        };

                        // 粉体 PLC 物料编码值转换成赣锋物料编码
                        var mat = GlueMaterials.FirstOrDefault(p => p.PowderPlcValue == detail.MaterialCode);
                        if (mat is not null)
                            detail.MaterialCode = (short)mat.HongYunValue;

                        //detail.MaterialCode = ConvertMaterialCode(detail.MaterialCode, false); // 将红运物料编码转换成赣锋物料编码
                        detail.StepSerialNumber = stepNumber;

                        details.Add(detail);
                        stepNumber++;
                    }
                    recipe.Details = details.ToArray();
                    log.ResponseContent = System.Text.Json.JsonSerializer.Serialize(recipe);
                    LogMes(MesVisitSucceed, log);
                    return Ok(recipe);
                }
                else if (device.FeatureType == PlcFeatureTypes.Dilution) // 读取上料系统 1500 CPU 的稀释配方
                {
                    var dilutionReadRecipes = Config.GetSection("RecipePoint:ReadRecipe:Dilution").Get<DilutionReadRecipe[]>();
                    var readRecipe = dilutionReadRecipes?.FirstOrDefault(r => r.DeviceCode == device_code);

                    int readDilutionDb = readRecipe?.DataBlock ?? throw new ArgumentNullException(nameof(readRecipe.DataBlock));
                    int recipeNameIndex = readRecipe.RecipeNameStart; // 配方名起始位置
                    int recipeNameLength = readRecipe.RecipeNameLength; // 配方名字节数
                    // 生成配方名字节流
                    var recipeName = string.Empty;
                    byte[] recipeNameBytes = _plc.ReadBytes(readDilutionDb, recipeNameIndex, recipeNameLength + 2);
                    if (recipeNameBytes[1] > 0)
                    {
                        byte[] nameBytes = new byte[recipeNameBytes[1]];
                        Array.Copy(recipeNameBytes, 2, nameBytes, 0, recipeNameBytes[1]);
                        recipeName = nameBytes.FromBytesToString(Encoding.UTF8);
                    }

                    int readMaterialRecipeRatio = readRecipe.WaterRecipeRatio;// 配比
                    MesRecipeResult recipe = new() { DeviceCode = device_code, Rate = _plc.ReadFloat($"DB{readDilutionDb}.DBD{readMaterialRecipeRatio}"), Name = recipeName };

                    int materialWeight = readRecipe.MaterialWeight;// 物料重量
                    int waterWeight = readRecipe.WaterWeight;// 水重量
                    int stirRotateSpeed = readRecipe.StirRotateSpeed;// 搅拌转速
                    int stepElapsedTime = readRecipe.StepElapsedTime; // 工步运行时间
                    int vacuum = readRecipe.Vacuum; // 真空

                    List<MesRecipeResultDetail> details = new();
                    MesRecipeResultDetail step1 = new() { EndSymbol = false, StepSerialNumber = 1, FunctionCode = 0, MaterialCode = _dilutionStep1Material, MaterialWeight = _plc.ReadFloat($"DB{readDilutionDb}.DBD{materialWeight}") };
                    MesRecipeResultDetail step2 = new() { EndSymbol = false, StepSerialNumber = 2, FunctionCode = 1, MaterialCode = _dilutionStep2Material, MaterialWeight = _plc.ReadFloat($"DB{readDilutionDb}.DBD{waterWeight}") };
                    MesRecipeResultDetail step3 = new() { EndSymbol = false, StepSerialNumber = 3, FunctionCode = 1, MaterialCode = 0, StirRotateSpeed = (short)_plc.ReadInt($"DB{readDilutionDb}.DBW{stirRotateSpeed}"), ElapsedTime = (short)_plc.ReadInt($"DB{readDilutionDb}.DBW{stepElapsedTime}"), Vacuum = _plc.ReadFloat($"DB{readDilutionDb}.DBD{vacuum}") };

                    using StreamReader sr = new($"{AppDataFolder}\\{device_code}_recipe");
                    string? line = sr.ReadLine();
                    sr.Close();
                    if (line is not null)
                    {
                        var record = JsonSerializer.Deserialize<MesRecipeArgs>(line);
                        if (record is not null && record.Details?.Count > 2)
                        {
                            step1.MaterialCode = record.Details[0].MaterialCode;
                            step1.MaterialWeight = record.Details[0].MaterialWeight;
                            step1.FunctionCode = record.Details[0].FunctionCode;
                            step2.MaterialCode = record.Details[1].MaterialCode;
                            step2.MaterialWeight = record.Details[1].MaterialWeight;
                            step2.FunctionCode = record.Details[1].FunctionCode;
                            step3.FunctionCode = record.Details[2].FunctionCode;
                            step3.Temperature = record.Details[2].Temperature;
                            step3.Vacuum = record.Details[2].Vacuum;
                        }
                    }

                    //MesRecipeResultDetail step4 = new() { EndSymbol = true, StepSerialNumber = 4, FunctionCode = 0, MaterialCode = 0, StirRotateSpeed = 10, ElapsedTime = 0, Vacuum = -36 };

                    details.Add(step1);
                    details.Add(step2);
                    details.Add(step3);

                    //details.Add(step4);

                    recipe.Details = details.ToArray();
                    log.ResponseContent = JsonSerializer.Serialize(recipe);
                    LogMes(MesVisitSucceed, log);
                    return Ok(recipe);
                }
                else
                {
                    return BadResult(3, log);
                }
            }
            catch (EnsureArgsAndCovertMaterialCodeException ex)
            {
                return BadResult(ex.Code, log);
            }
            catch (GetDevicePlcException ex)
            {
                return BadResult(ex.Code, log);
            }
            catch (Exception ex)
            {
                int code = 6;
                switch (ex.Message)
                {
                    case nameof(S7ProtocolReadException):
                    case nameof(S7ProtocolWriteException):
                        code = 15;
                        break;
                    default:
                        break;
                }
                return ExceptionResult(code, ex, log);
            }
            finally
            {
                _plc?.Dispose();
            }
        }

        string? GetMaterialMatchingAddress(short materialCode)
        {
            if (MaterialCodes == null)
                return null;

            var material = MaterialCodes.FirstOrDefault(p => p.HongYunValue == materialCode);
            if (material == null)
                return null;

            return material.MatchingAddress;

        }

        List<byte> GetStringBytes(string text, byte definedLength)
        {
            List<byte> resultBytes = new();
            resultBytes.Add(definedLength); // 第一个字节：字段长度
            byte[] temp = Encoding.UTF8.GetBytes(text);
            resultBytes.Add((byte)temp.Length); // 第二个字节：字符串字节数实际长度
            resultBytes.AddRange(temp); // 实际字符串字节

            byte[] empties = new byte[definedLength + 2 - resultBytes.Count];
            resultBytes.AddRange(empties);

            return resultBytes;
        }

        byte[] ConvertBoolsToBytes(List<bool> bools)
        {
            BitArray bits = new(bools.ToArray());
            const int BYTE = 8;
            int length = (bits.Count / BYTE) + ((bits.Count % BYTE == 0) ? 0 : 1);
            var bytes = new byte[length];

            for (int i = 0; i < bits.Length; i++)
            {

                int bitIndex = i % BYTE;
                int byteIndex = i / BYTE;

                int mask = (bits[i] ? 1 : 0) << bitIndex;
                bytes[byteIndex] |= (byte)mask;

            }

            return bytes;

        }
    }
}
